package com.hqd.ch03.v34.web.utils;

import com.hqd.ch03.utils.CollectionUtils;
import com.hqd.ch03.utils.LinkedMultiValueMap;
import com.hqd.ch03.utils.MultiValueMap;
import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.v34.utils.StringUtils;
import com.hqd.ch03.v34.web.http.HttpHeaders;
import com.hqd.ch03.v34.web.http.HttpRequest;

import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UriComponentsBuilder implements UriBuilder, Cloneable {
    private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#]*)";
    private static final String PATH_PATTERN = "([^?#]*)";
    private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");

    private static final String SCHEME_PATTERN = "([^:/?#]+):";

    private static final String HTTP_PATTERN = "(?i)(http|https):";

    private static final String USERINFO_PATTERN = "([^@\\[/?#]*)";

    private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*";
    private static final String LAST_PATTERN = "(.*)";
    private static final String FORWARDED_VALUE = "\"?([^;,\"]+)\"?";

    private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]";
    private static final String QUERY_PATTERN = "([^#]*)";
    private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")";
    // Regex patterns that matches URIs. See RFC 3986, appendix B
    private static final Pattern URI_PATTERN = Pattern.compile(
            "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
                    ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
    private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
            "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
                    PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");

    private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("(?i:host)=" + FORWARDED_VALUE);

    private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("(?i:proto)=" + FORWARDED_VALUE);

    private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=" + FORWARDED_VALUE);

    private static final Object[] EMPTY_VALUES = new Object[0];
    private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
    private final Map<String, Object> uriVariables = new HashMap<>(4);

    private String scheme;

    private String ssp;

    private String userInfo;

    private String host;

    private String port;
    private CompositePathComponentBuilder pathBuilder;

    private String fragment;
    private boolean encodeTemplate;

    private Charset charset = StandardCharsets.UTF_8;


    /**
     * Default constructor. Protected to prevent direct instantiation.
     *
     * @see #newInstance()
     * @see #fromPath(String)
     * @see #fromUri(URI)
     */
    protected UriComponentsBuilder() {
        this.pathBuilder = new CompositePathComponentBuilder();
    }

    /**
     * Create a deep copy of the given UriComponentsBuilder.
     *
     * @param other the other builder to copy from
     * @since 4.1.3
     */
    protected UriComponentsBuilder(UriComponentsBuilder other) {
        this.scheme = other.scheme;
        this.ssp = other.ssp;
        this.userInfo = other.userInfo;
        this.host = other.host;
        this.port = other.port;
        this.pathBuilder = other.pathBuilder.cloneBuilder();
        this.uriVariables.putAll(other.uriVariables);
        this.queryParams.addAll(other.queryParams);
        this.fragment = other.fragment;
        this.encodeTemplate = other.encodeTemplate;
        this.charset = other.charset;
    }


    // Factory methods

    /**
     * Create a new, empty builder.
     *
     * @return the new {@code UriComponentsBuilder}
     */
    public static UriComponentsBuilder newInstance() {
        return new UriComponentsBuilder();
    }

    /**
     * Create a builder that is initialized with the given path.
     *
     * @param path the path to initialize with
     * @return the new {@code UriComponentsBuilder}
     */
    public static UriComponentsBuilder fromPath(String path) {
        UriComponentsBuilder builder = new UriComponentsBuilder();
        builder.path(path);
        return builder;
    }

    public static UriComponentsBuilder fromUri(URI uri) {
        UriComponentsBuilder builder = new UriComponentsBuilder();
        builder.uri(uri);
        return builder;
    }

    /**
     * Create a builder that is initialized with the given URI string.
     * <p><strong>Note:</strong> The presence of reserved characters can prevent
     * correct parsing of the URI string. For example if a query parameter
     * contains {@code '='} or {@code '&'} characters, the query string cannot
     * be parsed unambiguously. Such values should be substituted for URI
     * variables to enable correct parsing:
     * <pre class="code">
     * String uriString = &quot;/hotels/42?filter={value}&quot;;
     * UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
     * </pre>
     *
     * @param uri the URI string to initialize with
     * @return the new {@code UriComponentsBuilder}
     */
    public static UriComponentsBuilder fromUriString(String uri) {
        Matcher matcher = URI_PATTERN.matcher(uri);
        if (matcher.matches()) {
            UriComponentsBuilder builder = new UriComponentsBuilder();
            String scheme = matcher.group(2);
            String userInfo = matcher.group(5);
            String host = matcher.group(6);
            String port = matcher.group(8);
            String path = matcher.group(9);
            String query = matcher.group(11);
            String fragment = matcher.group(13);
            boolean opaque = false;
            if (StringUtils.hasLength(scheme)) {
                String rest = uri.substring(scheme.length());
                if (!rest.startsWith(":/")) {
                    opaque = true;
                }
            }
            builder.scheme(scheme);
            if (opaque) {
                String ssp = uri.substring(scheme.length() + 1);
                if (StringUtils.hasLength(fragment)) {
                    ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1));
                }
                builder.schemeSpecificPart(ssp);
            } else {
                if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
                    throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
                }
                builder.userInfo(userInfo);
                builder.host(host);
                if (StringUtils.hasLength(port)) {
                    builder.port(port);
                }
                builder.path(path);
                builder.query(query);
            }
            if (StringUtils.hasText(fragment)) {
                builder.fragment(fragment);
            }
            return builder;
        } else {
            throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
        }
    }

    public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
        Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
        if (matcher.matches()) {
            UriComponentsBuilder builder = new UriComponentsBuilder();
            String scheme = matcher.group(1);
            builder.scheme(scheme != null ? scheme.toLowerCase() : null);
            builder.userInfo(matcher.group(4));
            String host = matcher.group(5);
            if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
                throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
            }
            builder.host(host);
            String port = matcher.group(7);
            if (StringUtils.hasLength(port)) {
                builder.port(port);
            }
            builder.path(matcher.group(8));
            builder.query(matcher.group(10));
            String fragment = matcher.group(12);
            if (StringUtils.hasText(fragment)) {
                builder.fragment(fragment);
            }
            return builder;
        } else {
            throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
        }
    }

    public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {
        return fromUri(request.getURI()).adaptFromForwardedHeaders(request.getHeaders());
    }


    public static InetSocketAddress parseForwardedFor(
            HttpRequest request, InetSocketAddress remoteAddress) {

        int port = (remoteAddress != null ?
                remoteAddress.getPort() : "https".equals(request.getURI().getScheme()) ? 443 : 80);

        String forwardedHeader = request.getHeaders().getFirst("Forwarded");
        if (StringUtils.hasText(forwardedHeader)) {
            String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
            Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse);
            if (matcher.find()) {
                String value = matcher.group(1).trim();
                String host = value;
                int portSeparatorIdx = value.lastIndexOf(':');
                int squareBracketIdx = value.lastIndexOf(']');
                if (portSeparatorIdx > squareBracketIdx) {
                    if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
                        throw new IllegalArgumentException("Invalid IPv4 address: " + value);
                    }
                    host = value.substring(0, portSeparatorIdx);
                    try {
                        port = Integer.parseInt(value.substring(portSeparatorIdx + 1));
                    } catch (NumberFormatException ex) {
                        throw new IllegalArgumentException(
                                "Failed to parse a port from \"forwarded\"-type header value: " + value);
                    }
                }
                return InetSocketAddress.createUnresolved(host, port);
            }
        }

        String forHeader = request.getHeaders().getFirst("X-Forwarded-For");
        if (StringUtils.hasText(forHeader)) {
            String host = StringUtils.tokenizeToStringArray(forHeader, ",")[0];
            return InetSocketAddress.createUnresolved(host, port);
        }

        return null;
    }

    /**
     * Create an instance by parsing the "Origin" header of an HTTP request.
     *
     * @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454</a>
     */
    public static UriComponentsBuilder fromOriginHeader(String origin) {
        Matcher matcher = URI_PATTERN.matcher(origin);
        if (matcher.matches()) {
            UriComponentsBuilder builder = new UriComponentsBuilder();
            String scheme = matcher.group(2);
            String host = matcher.group(6);
            String port = matcher.group(8);
            if (StringUtils.hasLength(scheme)) {
                builder.scheme(scheme);
            }
            builder.host(host);
            if (StringUtils.hasLength(port)) {
                builder.port(port);
            }
            return builder;
        } else {
            throw new IllegalArgumentException("[" + origin + "] is not a valid \"Origin\" header value");
        }
    }


    public final UriComponentsBuilder encode() {
        return encode(StandardCharsets.UTF_8);
    }

    /**
     * A variant of {@link #encode()} with a charset other than "UTF-8".
     *
     * @param charset the charset to use for encoding
     * @since 5.0.8
     */
    public UriComponentsBuilder encode(Charset charset) {
        this.encodeTemplate = true;
        this.charset = charset;
        return this;
    }


    // Build methods

    /**
     * Build a {@code UriComponents} instance from the various components contained in this builder.
     *
     * @return the URI components
     */
    public UriComponents build() {
        return build(false);
    }

    public UriComponents build(boolean encoded) {
        return buildInternal(encoded ? EncodingHint.FULLY_ENCODED :
                (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE));
    }

    private UriComponents buildInternal(EncodingHint hint) {
        UriComponents result;
        if (this.ssp != null) {
            result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment);
        } else {
            HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment,
                    this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams,
                    hint == EncodingHint.FULLY_ENCODED);
            result = (hint == EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric);
        }
        if (!this.uriVariables.isEmpty()) {
            result = result.expand(name -> this.uriVariables.getOrDefault(name, UriComponents.UriTemplateVariables.SKIP_VALUE));
        }
        return result;
    }

    /**
     * Build a {@code UriComponents} instance and replaces URI template variables
     * with the values from a map. This is a shortcut method which combines
     * calls to {@link #build()} and then {@link UriComponents#expand(Map)}.
     *
     * @param uriVariables the map of URI variables
     * @return the URI components with expanded values
     */
    public UriComponents buildAndExpand(Map<String, ?> uriVariables) {
        return build().expand(uriVariables);
    }

    /**
     * Build a {@code UriComponents} instance and replaces URI template variables
     * with the values from an array. This is a shortcut method which combines
     * calls to {@link #build()} and then {@link UriComponents#expand(Object...)}.
     *
     * @param uriVariableValues the URI variable values
     * @return the URI components with expanded values
     */
    public UriComponents buildAndExpand(Object... uriVariableValues) {
        return build().expand(uriVariableValues);
    }

    @Override
    public URI build(Object... uriVariables) {
        return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
    }

    @Override
    public URI build(Map<String, ?> uriVariables) {
        return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
    }

    /**
     * Build a URI String.
     * <p>Effectively, a shortcut for building, encoding, and returning the
     * String representation:
     * <pre class="code">
     * String uri = builder.build().encode().toUriString()
     * </pre>
     * <p>However if {@link #uriVariables(Map) URI variables} have been provided
     * then the URI template is pre-encoded separately from URI variables (see
     * {@link #encode()} for details), i.e. equivalent to:
     * <pre>
     * String uri = builder.encode().build().toUriString()
     * </pre>
     *
     * @see UriComponents#toUriString()
     * @since 4.1
     */
    public String toUriString() {
        return (this.uriVariables.isEmpty() ? build().encode().toUriString() :
                buildInternal(EncodingHint.ENCODE_TEMPLATE).toUriString());
    }


    // Instance methods

    /**
     * Initialize components of this builder from components of the given URI.
     *
     * @param uri the URI
     * @return this UriComponentsBuilder
     */
    public UriComponentsBuilder uri(URI uri) {
        this.scheme = uri.getScheme();
        if (uri.isOpaque()) {
            this.ssp = uri.getRawSchemeSpecificPart();
            resetHierarchicalComponents();
        } else {
            if (uri.getRawUserInfo() != null) {
                this.userInfo = uri.getRawUserInfo();
            }
            if (uri.getHost() != null) {
                this.host = uri.getHost();
            }
            if (uri.getPort() != -1) {
                this.port = String.valueOf(uri.getPort());
            }
            if (StringUtils.hasLength(uri.getRawPath())) {
                this.pathBuilder = new CompositePathComponentBuilder();
                this.pathBuilder.addPath(uri.getRawPath());
            }
            if (StringUtils.hasLength(uri.getRawQuery())) {
                this.queryParams.clear();
                query(uri.getRawQuery());
            }
            resetSchemeSpecificPart();
        }
        if (uri.getRawFragment() != null) {
            this.fragment = uri.getRawFragment();
        }
        return this;
    }

    /**
     * Set or append individual URI components of this builder from the values
     * of the given {@link UriComponents} instance.
     * <p>For the semantics of each component (i.e. set vs append) check the
     * builder methods on this class. For example {@link #host(String)} sets
     * while {@link #path(String)} appends.
     *
     * @param uriComponents the UriComponents to copy from
     * @return this UriComponentsBuilder
     */
    public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
        uriComponents.copyToUriComponentsBuilder(this);
        return this;
    }

    @Override
    public UriComponentsBuilder scheme(String scheme) {
        this.scheme = scheme;
        return this;
    }

    /**
     * Set the URI scheme-specific-part. When invoked, this method overwrites
     * {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host},
     * {@linkplain #port(int) port}, {@linkplain #path(String) path}, and
     * {@link #query(String) query}.
     *
     * @param ssp the URI scheme-specific-part, may contain URI template parameters
     * @return this UriComponentsBuilder
     */
    public UriComponentsBuilder schemeSpecificPart(String ssp) {
        this.ssp = ssp;
        resetHierarchicalComponents();
        return this;
    }

    @Override
    public UriComponentsBuilder userInfo(String userInfo) {
        this.userInfo = userInfo;
        resetSchemeSpecificPart();
        return this;
    }

    @Override
    public UriComponentsBuilder host(String host) {
        this.host = host;
        if (host != null) {
            resetSchemeSpecificPart();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder port(int port) {
        this.port = String.valueOf(port);
        if (port > -1) {
            resetSchemeSpecificPart();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder port(String port) {
        this.port = port;
        if (port != null) {
            resetSchemeSpecificPart();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder path(String path) {
        this.pathBuilder.addPath(path);
        resetSchemeSpecificPart();
        return this;
    }

    @Override
    public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
        this.pathBuilder.addPathSegments(pathSegments);
        resetSchemeSpecificPart();
        return this;
    }

    @Override
    public UriComponentsBuilder replacePath(String path) {
        this.pathBuilder = new CompositePathComponentBuilder();
        if (path != null) {
            this.pathBuilder.addPath(path);
        }
        resetSchemeSpecificPart();
        return this;
    }

    @Override
    public UriComponentsBuilder query(String query) {
        if (query != null) {
            Matcher matcher = QUERY_PARAM_PATTERN.matcher(query);
            while (matcher.find()) {
                String name = matcher.group(1);
                String eq = matcher.group(2);
                String value = matcher.group(3);
                queryParam(name, (value != null ? value : (StringUtils.hasLength(eq) ? "" : null)));
            }
            resetSchemeSpecificPart();
        } else {
            this.queryParams.clear();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder replaceQuery(String query) {
        this.queryParams.clear();
        if (query != null) {
            query(query);
            resetSchemeSpecificPart();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder queryParam(String name, Object... values) {
        if (!ObjectUtils.isEmpty(values)) {
            for (Object value : values) {
                String valueAsString = getQueryParamValue(value);
                this.queryParams.add(name, valueAsString);
            }
        } else {
            this.queryParams.add(name, null);
        }
        resetSchemeSpecificPart();
        return this;
    }


    private String getQueryParamValue(Object value) {
        if (value != null) {
            return (value instanceof Optional ?
                    ((Optional<?>) value).map(Object::toString).orElse(null) :
                    value.toString());
        }
        return null;
    }

    @Override
    public UriComponentsBuilder queryParam(String name, Collection<?> values) {
        return queryParam(name, (CollectionUtils.isEmpty(values) ? EMPTY_VALUES : values.toArray()));
    }

    @Override
    public UriComponentsBuilder queryParamIfPresent(String name, Optional<?> value) {
        value.ifPresent(o -> {
            if (o instanceof Collection) {
                queryParam(name, (Collection<?>) o);
            } else {
                queryParam(name, o);
            }
        });
        return this;
    }

    /**
     * {@inheritDoc}
     *
     * @since 4.0
     */
    @Override
    public UriComponentsBuilder queryParams(MultiValueMap<String, String> params) {
        if (params != null) {
            this.queryParams.addAll(params);
            resetSchemeSpecificPart();
        }
        return this;
    }

    @Override
    public UriComponentsBuilder replaceQueryParam(String name, Object... values) {
        this.queryParams.remove(name);
        if (!ObjectUtils.isEmpty(values)) {
            queryParam(name, values);
        }
        resetSchemeSpecificPart();
        return this;
    }

    @Override
    public UriComponentsBuilder replaceQueryParam(String name, Collection<?> values) {
        return replaceQueryParam(name, (CollectionUtils.isEmpty(values) ? EMPTY_VALUES : values.toArray()));
    }

    /**
     * {@inheritDoc}
     *
     * @since 4.2
     */
    @Override
    public UriComponentsBuilder replaceQueryParams(MultiValueMap<String, String> params) {
        this.queryParams.clear();
        if (params != null) {
            this.queryParams.putAll(params);
        }
        return this;
    }

    @Override
    public UriComponentsBuilder fragment(String fragment) {
        if (fragment != null) {
            this.fragment = fragment;
        } else {
            this.fragment = null;
        }
        return this;
    }

    /**
     * Configure URI variables to be expanded at build time.
     * <p>The provided variables may be a subset of all required ones. At build
     * time, the available ones are expanded, while unresolved URI placeholders
     * are left in place and can still be expanded later.
     * <p>In contrast to {@link UriComponents#expand(Map)} or
     * {@link #buildAndExpand(Map)}, this method is useful when you need to
     * supply URI variables without building the {@link UriComponents} instance
     * just yet, or perhaps pre-expand some shared default values such as host
     * and port.
     *
     * @param uriVariables the URI variables to use
     * @return this UriComponentsBuilder
     * @since 5.0.8
     */
    public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {
        this.uriVariables.putAll(uriVariables);
        return this;
    }

    /**
     * Adapt this builder's scheme+host+port from the given headers, specifically
     * "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>,
     * or "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if
     * "Forwarded" is not found.
     * <p><strong>Note:</strong> this method uses values from forwarded headers,
     * if present, in order to reflect the client-originated protocol and address.
     * Consider using the {@code ForwardedHeaderFilter} in order to choose from a
     * central place whether to extract and use, or to discard such headers.
     * See the Spring Framework reference for more on this filter.
     *
     * @param headers the HTTP headers to consider
     * @return this UriComponentsBuilder
     * @since 4.2.7
     */
    UriComponentsBuilder adaptFromForwardedHeaders(HttpHeaders headers) {
        try {
            String forwardedHeader = headers.getFirst("Forwarded");
            if (StringUtils.hasText(forwardedHeader)) {
                Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedHeader);
                if (matcher.find()) {
                    scheme(matcher.group(1).trim());
                    port(null);
                } else if (isForwardedSslOn(headers)) {
                    scheme("https");
                    port(null);
                }
                matcher = FORWARDED_HOST_PATTERN.matcher(forwardedHeader);
                if (matcher.find()) {
                    adaptForwardedHost(matcher.group(1).trim());
                }
            } else {
                String protocolHeader = headers.getFirst("X-Forwarded-Proto");
                if (StringUtils.hasText(protocolHeader)) {
                    scheme(StringUtils.tokenizeToStringArray(protocolHeader, ",")[0]);
                    port(null);
                } else if (isForwardedSslOn(headers)) {
                    scheme("https");
                    port(null);
                }
                String hostHeader = headers.getFirst("X-Forwarded-Host");
                if (StringUtils.hasText(hostHeader)) {
                    adaptForwardedHost(StringUtils.tokenizeToStringArray(hostHeader, ",")[0]);
                }
                String portHeader = headers.getFirst("X-Forwarded-Port");
                if (StringUtils.hasText(portHeader)) {
                    port(Integer.parseInt(StringUtils.tokenizeToStringArray(portHeader, ",")[0]));
                }
            }
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Failed to parse a port from \"forwarded\"-type headers. " +
                    "If not behind a trusted proxy, consider using ForwardedHeaderFilter " +
                    "with the removeOnly=true. Request headers: " + headers);
        }

        if (this.scheme != null &&
                (((this.scheme.equals("http") || this.scheme.equals("ws")) && "80".equals(this.port)) ||
                        ((this.scheme.equals("https") || this.scheme.equals("wss")) && "443".equals(this.port)))) {
            port(null);
        }

        return this;
    }

    private boolean isForwardedSslOn(HttpHeaders headers) {
        String forwardedSsl = headers.getFirst("X-Forwarded-Ssl");
        return StringUtils.hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on");
    }

    private void adaptForwardedHost(String rawValue) {
        int portSeparatorIdx = rawValue.lastIndexOf(':');
        int squareBracketIdx = rawValue.lastIndexOf(']');
        if (portSeparatorIdx > squareBracketIdx) {
            if (squareBracketIdx == -1 && rawValue.indexOf(':') != portSeparatorIdx) {
                throw new IllegalArgumentException("Invalid IPv4 address: " + rawValue);
            }
            host(rawValue.substring(0, portSeparatorIdx));
            port(Integer.parseInt(rawValue.substring(portSeparatorIdx + 1)));
        } else {
            host(rawValue);
            port(null);
        }
    }

    private void resetHierarchicalComponents() {
        this.userInfo = null;
        this.host = null;
        this.port = null;
        this.pathBuilder = new CompositePathComponentBuilder();
        this.queryParams.clear();
    }

    private void resetSchemeSpecificPart() {
        this.ssp = null;
    }


    /**
     * Public declaration of Object's {@code clone()} method.
     * Delegates to {@link #cloneBuilder()}.
     */
    @Override
    public Object clone() {
        return cloneBuilder();
    }

    /**
     * Clone this {@code UriComponentsBuilder}.
     *
     * @return the cloned {@code UriComponentsBuilder} object
     * @since 4.2.7
     */
    public UriComponentsBuilder cloneBuilder() {
        return new UriComponentsBuilder(this);
    }


    private enum EncodingHint {ENCODE_TEMPLATE, FULLY_ENCODED, NONE}


    private interface PathComponentBuilder {


        HierarchicalUriComponents.PathComponent build();

        PathComponentBuilder cloneBuilder();
    }

    private static class CompositePathComponentBuilder implements PathComponentBuilder {

        private final Deque<PathComponentBuilder> builders = new ArrayDeque<>();

        public void addPathSegments(String... pathSegments) {
            if (!ObjectUtils.isEmpty(pathSegments)) {
                PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
                FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
                if (psBuilder == null) {
                    psBuilder = new PathSegmentComponentBuilder();
                    this.builders.add(psBuilder);
                    if (fpBuilder != null) {
                        fpBuilder.removeTrailingSlash();
                    }
                }
                psBuilder.append(pathSegments);
            }
        }

        public void addPath(String path) {
            if (StringUtils.hasText(path)) {
                PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
                FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
                if (psBuilder != null) {
                    path = (path.startsWith("/") ? path : "/" + path);
                }
                if (fpBuilder == null) {
                    fpBuilder = new FullPathComponentBuilder();
                    this.builders.add(fpBuilder);
                }
                fpBuilder.append(path);
            }
        }

        @SuppressWarnings("unchecked")

        private <T> T getLastBuilder(Class<T> builderClass) {
            if (!this.builders.isEmpty()) {
                PathComponentBuilder last = this.builders.getLast();
                if (builderClass.isInstance(last)) {
                    return (T) last;
                }
            }
            return null;
        }

        @Override
        public HierarchicalUriComponents.PathComponent build() {
            int size = this.builders.size();
            List<HierarchicalUriComponents.PathComponent> components = new ArrayList<>(size);
            for (PathComponentBuilder componentBuilder : this.builders) {
                HierarchicalUriComponents.PathComponent pathComponent = componentBuilder.build();
                if (pathComponent != null) {
                    components.add(pathComponent);
                }
            }
            if (components.isEmpty()) {
                return HierarchicalUriComponents.NULL_PATH_COMPONENT;
            }
            if (components.size() == 1) {
                return components.get(0);
            }
            return new HierarchicalUriComponents.PathComponentComposite(components);
        }

        @Override
        public CompositePathComponentBuilder cloneBuilder() {
            CompositePathComponentBuilder compositeBuilder = new CompositePathComponentBuilder();
            for (PathComponentBuilder builder : this.builders) {
                compositeBuilder.builders.add(builder.cloneBuilder());
            }
            return compositeBuilder;
        }
    }

    private static class FullPathComponentBuilder implements PathComponentBuilder {

        private final StringBuilder path = new StringBuilder();

        private static String getSanitizedPath(final StringBuilder path) {
            int index = path.indexOf("//");
            if (index >= 0) {
                StringBuilder sanitized = new StringBuilder(path);
                while (index != -1) {
                    sanitized.deleteCharAt(index);
                    index = sanitized.indexOf("//", index);
                }
                return sanitized.toString();
            }
            return path.toString();
        }

        public void append(String path) {
            this.path.append(path);
        }

        @Override
        public HierarchicalUriComponents.PathComponent build() {
            if (this.path.length() == 0) {
                return null;
            }
            String sanitized = getSanitizedPath(this.path);
            return new HierarchicalUriComponents.FullPathComponent(sanitized);
        }

        public void removeTrailingSlash() {
            int index = this.path.length() - 1;
            if (this.path.charAt(index) == '/') {
                this.path.deleteCharAt(index);
            }
        }

        @Override
        public FullPathComponentBuilder cloneBuilder() {
            FullPathComponentBuilder builder = new FullPathComponentBuilder();
            builder.append(this.path.toString());
            return builder;
        }
    }

    private static class PathSegmentComponentBuilder implements PathComponentBuilder {

        private final List<String> pathSegments = new ArrayList<>();

        public void append(String... pathSegments) {
            for (String pathSegment : pathSegments) {
                if (StringUtils.hasText(pathSegment)) {
                    this.pathSegments.add(pathSegment);
                }
            }
        }

        @Override
        public HierarchicalUriComponents.PathComponent build() {
            return (this.pathSegments.isEmpty() ? null :
                    new HierarchicalUriComponents.PathSegmentComponent(this.pathSegments));
        }

        @Override
        public PathSegmentComponentBuilder cloneBuilder() {
            PathSegmentComponentBuilder builder = new PathSegmentComponentBuilder();
            builder.pathSegments.addAll(this.pathSegments);
            return builder;
        }
    }

}